﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using CSharp4ExpressionMetaCompiler;
using System.Linq.Expressions;
using SStuff.MetaEffects.Model;
using System.Reflection;

namespace SStuff.MetaEffects.Compiler
{
    public static class ExpressionToHlsl
    {
        public static string TranslateToHLSL<T>(Expression<Action<T>> expression)
        {
            return new ExpressionToHlslCompiler(new Expression[] {expression}, new Type[]{typeof(T)}).TranslateToHLSL();
        }

        public static string TranslateToHLSL(params Expression[] expressions)
        {
            List<Type> types = new List<Type>();
            foreach(var exp in expressions)
            {
                if (!(exp is LambdaExpression))
                {
                    throw new ArgumentOutOfRangeException("expressions",
                                                          "All expressions composing the shader must be lambda expressions");
                }
                var asLambda = exp as LambdaExpression;
                if(asLambda.Parameters.Count!= 1)
                {
                    throw new ArgumentOutOfRangeException("expressions",
                                                          "Each lambda expression must have only one parameter");
                }
                types.Add(asLambda.Parameters[0].Type);
            }
            return new ExpressionToHlslCompiler(expressions, types.ToArray()).TranslateToHLSL();
        }
    }
    internal class ExpressionToHlslCompiler
    {
        Expression[] _expressions;
        private Type[] _inputTypes;
        public ExpressionToHlslCompiler(Expression[] expressions, Type[] inputTypes)
        {
            _expressions = expressions.Select(ex=>ExpressionHelper.Translate(ex)).ToArray();
            _inputTypes = inputTypes;
            
        }

        public string TranslateToHLSL()
        {
            StringBuilder sb = new StringBuilder();
            WriteHeader(sb);
            sb.Append(WriteBody());
            WriteFooter(sb);
            return sb.ToString();
        }

        private void WriteFooter(StringBuilder sb)
        {
            sb.AppendLine("}");
        }
        private string WriteBody()
        {
            StringBuilder sb = new StringBuilder();
            foreach(var exp in _expressions)
            {
                if (!EndsWithStatement(exp))
                    sb.Append(WriteStatement(exp));
                else
                    sb.Append(WriteBody(exp));
            }
            return sb.ToString();
        }
        private string WriteBody(Expression body)
        {
            switch (body.NodeType)
            {
                case ExpressionType.Block:
                    return WriteBlock(body as BlockExpression);
                    break;
                case ExpressionType.Lambda:
                    return WriteBody((body as LambdaExpression).Body);
                    break;
                case ExpressionType.Call:
                    return WriteCall(body as MethodCallExpression);
                    break;
                case ExpressionType.New:
                    return WriteNew(body as NewExpression);
                    break;
                case ExpressionType.MemberAccess:
                    return WriteMemberAccess(body as MemberExpression);
                    break;
                case ExpressionType.Add:
                    return WriteOperation(body as BinaryExpression, "+");
                    break;
                case ExpressionType.Subtract:
                    return WriteOperation(body as BinaryExpression, "-");
                    break;
                case ExpressionType.Multiply:
                    return WriteOperation(body as BinaryExpression, "*");
                    break;
                case ExpressionType.Divide:
                    return WriteOperation(body as BinaryExpression, "/");
                    break;
                case ExpressionType.Constant:
                    return Convert.ToString( (body as ConstantExpression).Value, CultureInfo.InvariantCulture);
                default:
                    throw new NotSupportedException(body.ToString());
            }
        }

        private string WriteOperation(BinaryExpression binaryExpression, string op)
        {
            return string.Format("({0} {1} {2})", WriteBody(binaryExpression.Left), op, WriteBody(binaryExpression.Right));
        }

        private static PropertyInfo _blockDefaultProperty = typeof (Block).GetProperty("Default");

        private static bool IsBlockDefault(Expression expr)
        {
            while(expr != null && expr.NodeType == ExpressionType.Lambda)
            {
                expr = (expr as LambdaExpression).Body;
            }
            if(expr !=null && expr.NodeType== ExpressionType.MemberAccess)
            {
                return (expr as MemberExpression).Member == _blockDefaultProperty;
            }
            return false;
        }

        private string WriteMemberAccess(MemberExpression memberExpression)
        {
            if(memberExpression.Member==_blockDefaultProperty)
            {
                return string.Empty;
            }
            else if(_inputTypes.Contains( memberExpression.Expression.Type))
            {
                // only member (as it is a variable access)
                return memberExpression.Member.Name;
            }
            else
            {
                return string.Format("{0}.{1}", WriteBody(memberExpression.Expression), memberExpression.Member.Name);
            }
        }

        private string WriteNew(NewExpression newExpression)
        {
            StringBuilder sb = new StringBuilder();
            sb.AppendFormat("{0}(", newExpression.Type.Name);

            bool first = true;
            foreach (var argument in newExpression.Arguments)
            {
                if (first)
                    first = false;
                else sb.Append(", ");

                sb.Append(WriteBody(argument));
            }

            sb.Append(") ");
            return sb.ToString();
        }

        private static MethodInfo _statementMethod = typeof(Block).GetMethod("_");
        private static MethodInfo _returnMethod = typeof(float4).GetMethod("Return");
        private static MethodInfo _assignMethod = typeof(Block).GetMethod("Assign");

        private bool EndsWithStatement(Expression expr)
        {
            while (expr != null && (expr.NodeType == ExpressionType.Lambda || expr.NodeType == ExpressionType.Block))
            {
                if (expr.NodeType == ExpressionType.Lambda)
                {
                    expr = (expr as LambdaExpression).Body;
                }
                else
                {
                    expr = (expr as BlockExpression).Expressions.Last();
                }
            }
            var asMethodCall = expr as MethodCallExpression;
            if (asMethodCall == null)
                return false;
            return asMethodCall.Method == _statementMethod;
        }
        private string WriteCall(MethodCallExpression methodCallExpression)
        {
            StringBuilder sb = new StringBuilder();
            if (methodCallExpression.Method == _statementMethod)
            {
                if (methodCallExpression.Object != null)
                {
                    sb.Append(WriteStatement(methodCallExpression.Object));
                }
                sb.Append(WriteStatement(methodCallExpression.Arguments[0]));
            }
            else if(methodCallExpression.Method==_returnMethod)
            {
                sb.Append("return ");
                sb.Append (WriteBody(methodCallExpression.Object));
            }
            else if(methodCallExpression.Method.IsGenericMethod && methodCallExpression.Method.GetGenericMethodDefinition() == _assignMethod)
            {
                if (methodCallExpression.Object != null)
                    sb.Append(WriteStatement(methodCallExpression.Object));

                sb.Append(WriteBody(methodCallExpression.Arguments[0]));
                sb.Append(" = ");
                sb.Append(WriteBody(methodCallExpression.Arguments[1]));
            }
            else
            {
                var hlslAttr =
                    methodCallExpression.Method.GetCustomAttributes(false).OfType<HlslFunctionAttribute>().
                        FirstOrDefault();
                if (hlslAttr == null)
                    throw new NotSupportedException("Supported method calls must have an HlslFunction attribute");

                string call = hlslAttr.Format;
                if(methodCallExpression.Object !=null)
                {
                    call = call.Replace("{this}", WriteBody(methodCallExpression.Object));
                }

                var parameters = methodCallExpression.Method.GetParameters();
                for(int i=0;i<parameters.Length;i++)
                {
                    call = call.Replace(string.Format("{{{0}}}", parameters[i].Name),
                                 WriteBody(methodCallExpression.Arguments[0]));
                }
                sb.Append(call);
            }

            return sb.ToString();
        }

        private string WriteStatement(Expression expression)
        {
            if (IsBlockDefault(expression))
                return string.Empty;
            return  WriteBody(expression) +";\r\n";
        }

        private string WriteBlock(BlockExpression blockExpression)
        {
            var sb = new StringBuilder();
            foreach (var expr in blockExpression.Expressions)
            {
                sb.Append(WriteBody(expr));
            }
            return sb.ToString();
        }

        private void WriteHeader(StringBuilder sb)
        {
            List<Tuple<string, int>> registeredConstants = new List<Tuple<string, int>>();

            List<string> declaredVariables = new List<string>();
            foreach (var p in _inputTypes.SelectMany(t=>t.GetProperties()))
            {
                var registerAttr = p.GetCustomAttributes(false).OfType<RegisterAttribute>().FirstOrDefault();
                if (registerAttr != null)
                {
                    var constantRegistration = Tuple.Create(p.Name, registerAttr.Index);
                    if (registeredConstants.Contains(constantRegistration))
                        continue;
                    sb.AppendFormat("{0} {1} : register({2}{3});", GetPropertyTypeName(p.PropertyType), p.Name, p.PropertyType == typeof(sampler2D) ? "S" : "c", registerAttr.Index);
                    sb.AppendLine();
                    declaredVariables.Add(p.Name);
                    registeredConstants.Add(constantRegistration);
                }
            }
            sb.AppendLine();
            var texCoordProp = _inputTypes.SelectMany(t => t.GetProperties()).Where(p => p.GetCustomAttributes(false).OfType<TexcoordAttribute>().Any()).First();
            declaredVariables.Add(texCoordProp.Name);

            

            sb.AppendFormat("float4 main(float2 {0} : TEXCOORD) : COLOR", texCoordProp.Name);
            sb.AppendLine();
            sb.AppendLine("{");

            foreach (var p in _inputTypes.SelectMany(t => t.GetProperties()).Where(p => !declaredVariables.Contains(p.Name)))
            {
                sb.AppendFormat("{0} {1};", GetPropertyTypeName(p.PropertyType), p.Name);
                sb.AppendLine();
                declaredVariables.Add(p.Name);
            }
        }
        private string GetPropertyTypeName(Type propertyType)
        {
            if (propertyType == typeof(int))
                return "int";
            else if (propertyType == typeof(uint))
                return "uint";
            else if (propertyType == typeof(bool))
                return "bool";

            else if (propertyType == typeof(float))
                return "float";
            else
                return propertyType.Name;
        }
    }
}
